﻿
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;

namespace EmperialApps.WeatherSpark.Data.Internal {

    internal static class Extensions {

        public static ReadOnlyCollection<T> ToReadOnly<T>( this IList<T> collection ) {
            return collection as ReadOnlyCollection<T>
                ?? new ReadOnlyCollection<T>( collection );
        }

        #region Serialization

        private static readonly int[] _factors = new int[] { 1, 10, 100, 1000 };

        private static short Serialize( double value, sbyte precision ) {
            double adjustedValue = value;

            if( double.IsNaN( value ) ) {
                adjustedValue = short.MaxValue;
            }
            else if( precision > 0 ) {
                int factor = _factors[precision];
                adjustedValue *= factor;
            }
            else if( precision < 0 ) {
                int factor = _factors[-precision];
                adjustedValue /= factor;
            }

            try {
                short rawValue = checked( (short)Math.Round( adjustedValue ) );
                return rawValue;
            }
            catch( ArithmeticException ex ) {
                string message = string.Format( "Could not encode value {0} with precision {1}.", value, precision );
                throw new ArithmeticException( message, ex );
            }
        }

        private static double DeSerialize( short rawValue, sbyte precision ) {
            double value = rawValue;

            if( rawValue == short.MaxValue ) {
                value = double.NaN;
            }
            else if( precision > 0 ) {
                int factor = _factors[precision];
                value /= factor;
            }
            else if( precision < 0 ) {
                int factor = _factors[-precision];
                value *= factor;
            }

            return value;
        }


        public static double ToPrecision( this double value, sbyte precision ) {
            short rawValue = Serialize( value, precision );
            double roundedValue = DeSerialize( rawValue, precision );
            return roundedValue;
        }

        public static void Write( this BinaryWriter writer, double value, sbyte precision ) {
            WriteDouble( writer, value, precision );
        }

        private static void WriteDouble( BinaryWriter writer, double value, sbyte precision ) {
            short rawValue = Serialize( value, precision );
            writer.Write( rawValue );
        }

        public static double ReadDouble( this BinaryReader reader, sbyte precision ) {
            short rawValue = reader.ReadInt16( );
            double value = DeSerialize( rawValue, precision );
            return value;
        }

        public static void Write( this BinaryWriter writer, IList<double> collection, sbyte precision ) {
            WriteDoubleCollection( writer, collection, precision );
        }

        private static void WriteDoubleCollection( BinaryWriter writer, IList<double> collection, sbyte precision ) {
            writer.Write( precision );
            writer.Write( (short)collection.Count );

            short previousValue = 0;
            foreach( double value in collection ) {
                short rawValue = Serialize( value, precision );

                // Write raw values as the difference from the previous value.
                int difference = rawValue - previousValue;
                if( difference > sbyte.MinValue && difference <= sbyte.MaxValue ) {
                    writer.Write( (sbyte)difference );
                }
                // Or, if difference is too large, store both sentinel and raw value.
                else {
                    writer.Write( sbyte.MinValue );
                    writer.Write( rawValue );
                }

                previousValue = rawValue;
            }
        }

        public static double[] ReadDoubleCollection( this BinaryReader reader ) {
            sbyte precision = reader.ReadSByte( );
            short count = reader.ReadInt16( );

            double[] values = new double[count];
            short previousValue = 0;
            for( int i = 0; i < count; ++i ) {
                sbyte difference = reader.ReadSByte( );
                short rawValue =
                    difference == sbyte.MinValue
                        ? reader.ReadInt16( )
                        : (short)(previousValue + difference);

                values[i] = DeSerialize( rawValue, precision );
                previousValue = rawValue;
            }

            return values;
        }

        public static void Write( this BinaryWriter writer, ForecastData value ) {
            WriteForecastData( writer, value );
        }

        private static void WriteForecastData( BinaryWriter writer, ForecastData value ) {
            writer.Write( (ushort)value );
        }

        public static ForecastData ReadForecastData( this BinaryReader reader ) {
            ushort value = reader.ReadUInt16( );
            return (ForecastData)value;
        }

        public static void Write( this BinaryWriter writer, ForecastTileMode value ) {
            WriteForecastTileMode( writer, value );
        }

        private static void WriteForecastTileMode( BinaryWriter writer, ForecastTileMode value ) {
            writer.Write( (ushort)value );
        }

        public static ForecastTileMode ReadForecastTileMode( this BinaryReader reader ) {
            int value = reader.ReadUInt16( );
            return (ForecastTileMode)value;
        }

        public static void Write( this BinaryWriter writer, ForecastSourceId value ) {
            WriteForecastSourceId( writer, value );
        }

        private static void WriteForecastSourceId( BinaryWriter writer, ForecastSourceId value ) {
            writer.Write( (ushort)value );
        }

        public static ForecastSourceId ReadForecastSourceId( this BinaryReader reader ) {
            int value = reader.ReadUInt16( );
            return (ForecastSourceId)value;
        }

        public static void Write( this BinaryWriter writer, Coordinate value ) {
            WriteCoordinate( writer, value );
        }

        private static void WriteCoordinate( BinaryWriter writer, Coordinate value ) {
            writer.Write( value.State );
        }

        public static Coordinate ReadCoordinate( this BinaryReader reader ) {
            uint state = reader.ReadUInt32( );
            return new Coordinate( state );
        }

        public static void Write( this BinaryWriter writer, TimeSpan value ) {
            WriteTimeSpan( writer, value );
        }

        private static void WriteTimeSpan( BinaryWriter writer, TimeSpan value ) {
            short minutes = checked( (short)value.TotalMinutes );
            writer.Write( minutes );
        }

        public static TimeSpan ReadTimeSpan( this BinaryReader reader ) {
            short minutes = reader.ReadInt16( );
            var value = TimeSpan.FromMinutes( minutes );
            return value;
        }

        public static void Write( this BinaryWriter writer, DateTimeOffset value ) {
            WriteTimeSpanOffset( writer, value );
        }

        private static void WriteTimeSpanOffset( BinaryWriter writer, DateTimeOffset value ) {
            var utc = value.ToUniversalTime( );
            writer.Write( utc.Ticks );
            writer.Write( value.Offset );
        }

        public static DateTimeOffset ReadDateTimeOffset( this BinaryReader reader ) {
            long ticks = reader.ReadInt64( );
            TimeSpan offset = reader.ReadTimeSpan( );

            var utc = new DateTimeOffset( ticks, TimeSpan.Zero );
            var value = utc.ToOffset( offset );
            return value;
        }

        #endregion

    }

}
